﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using OSGeo.MapGuide;
using System.Drawing;
using System.ComponentModel;
using System.IO;
using System.Drawing.Drawing2D;
using System.Diagnostics;
using System.Threading;
using System.Xml;
using System.Collections.Specialized;

namespace MapViewer
{
    //FIXME: Selections will currently result in a full re-render of the map. 
    //This is because I did initally try for two separate images (map + selection) 
    //to be rendered and to be merged at OnPaint(), but selection images were not 
    //correctly merging with box selections, and the selection images were not being
    //re-requested on change of zoom. We should revisit this ASAP because there is
    //a significant performance gain through this approach (only render selection when
    //one is made)

    public class MgMapViewer : Control, IMapViewer
    {
        private BackgroundWorker renderWorker;

        private MgResourceService _resSvc;
        private MgRenderingService _renderSvc;
        private MgdMap _map;
        private MgdSelection _selection;
        private MgRenderingOptions _overlayRenderOpts;
        //private MgRenderingOptions _selectionRenderOpts;
        private MgWktReaderWriter _wktRW;
        private MgAgfReaderWriter _agfRW;
        private MgGeometryFactory _geomFact;
        private MgMeasure _mapMeasure;

        private Color _mapBgColor;

        private bool firstRun = true;

        private double _orgX1;
        private double _orgX2;
        private double _orgY1;
        private double _orgY2;

        private double _extX1;
        private double _extX2;
        private double _extY1;
        private double _extY2;

        private Image _selectionImage;
        private Image _mapImage;

        internal Image Image
        {
            get { return _mapImage; }
            set
            {
                _mapImage = value;
                Invalidate();
            }
        }

        const double MINIMUM_ZOOM_SCALE = 5.0;

#if VIEWER_DEBUG
        private MgdLayer _debugLayer;

        private void CreateDebugFeatureSource()
        {
            var id = new MgDataPropertyDefinition("ID");
            id.DataType = MgPropertyType.Int32;
            id.Nullable = false;
            id.SetAutoGeneration(true);

            var geom = new MgGeometricPropertyDefinition("Geometry");
            geom.GeometryTypes = MgFeatureGeometricType.Point;
            geom.SpatialContextAssociation = "MapCs";

            var cls = new MgClassDefinition();
            cls.Name = "Debug";
            var props = cls.GetProperties();
            props.Add(id);
            props.Add(geom);

            var idProps = cls.GetIdentityProperties();
            idProps.Add(id);

            cls.DefaultGeometryPropertyName = "Geometry";

            var schema = new MgFeatureSchema("Default", "Default schema");
            var classes = schema.GetClasses();
            classes.Add(cls);

            //We can make anything up here, there's no real concept of sessions
            var sessionId = Guid.NewGuid().ToString();

            var debugFsId = new MgResourceIdentifier("Session:" + sessionId + "//Debug" + Guid.NewGuid().ToString() + ".FeatureSource");
            var createSdf = new MgCreateSdfParams("MapCs", _map.GetMapSRS(), schema);
            var featureSvc = MgServiceFactory.CreateFeatureService();
            var resSvc = MgServiceFactory.CreateResourceService();
            featureSvc.CreateFeatureSource(debugFsId, createSdf);

            byte[] bytes = Encoding.UTF8.GetBytes(string.Format(Debug.DebugLayer, debugFsId.ToString(), "Default:Debug", "Geometry"));
            var source = new MgByteSource(bytes, bytes.Length);

            var debugLayerId = new MgResourceIdentifier("Session:" + sessionId + "//" + debugFsId.Name + ".LayerDefinition");
            var breader = source.GetReader();
            resSvc.SetResource(debugLayerId, breader, null);
            
            _debugLayer = new MgdLayer(debugLayerId, resSvc);
            _debugLayer.SetLegendLabel("Debug Layer");
            _debugLayer.SetVisible(true);
            _debugLayer.SetDisplayInLegend(true);

            var mapLayers = _map.GetLayers();
            mapLayers.Insert(0, _debugLayer);

            UpdateCenterDebugPoint();
        }

        private MgPropertyCollection _debugCenter;

        private void UpdateCenterDebugPoint()
        {
            if (_debugCenter == null)
                _debugCenter = new MgPropertyCollection();

            var center = _wktRW.Read("POINT (" + _map.ViewCenter.Coordinate.X + " " + _map.ViewCenter.Coordinate.Y + ")");
            var agf = _agfRW.Write(center);
            if (!_debugCenter.Contains("Geometry"))
            {
                MgGeometryProperty geom = new MgGeometryProperty("Geometry", agf);
                _debugCenter.Add(geom);
            }
            else
            {
                MgGeometryProperty geom = (MgGeometryProperty)_debugCenter.GetItem("Geometry");
                geom.SetValue(agf);
            }

            int deleted = _debugLayer.DeleteFeatures("");
            Trace.TraceInformation("Deleted {0} debug points", deleted);
            var reader = _debugLayer.InsertFeatures(_debugCenter);
            int inserted = 0;
            while (reader.ReadNext())
            {
                inserted++;
            }
            reader.Close();
            Trace.TraceInformation("Added {0} debug points", inserted);
            _debugLayer.ForceRefresh();
        }
#endif

        private MgCoordinateSystem _mapCs;

        public MgMapViewer()
        {
            this.FeatureTooltipsEnabled = false;
            this.TooltipsEnabled = false;
            this.ZoomInFactor = 0.75;
            this.ZoomOutFactor = 1.35;

            this.ActiveTool = MapActiveTool.None;
            this.DoubleBuffered = true;
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.DoubleBuffer, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.DoubleBuffer, true);

            _mapBgColor = Color.Transparent;
            
            renderWorker = new BackgroundWorker();

            renderWorker.DoWork += renderWorker_DoWork;
            renderWorker.RunWorkerCompleted += renderWorker_RunWorkerCompleted;

            base.MouseUp += OnMapMouseUp;
            base.MouseMove += OnMapMouseMove;
            base.MouseDown += OnMapMouseDown;
            base.MouseClick += OnMapMouseClick;
            base.MouseDoubleClick += OnMapMouseDoubleClick;
            base.MouseHover += OnMapMouseHover;
            base.MouseEnter += OnMouseEnter;
        }

        void OnMouseEnter(object sender, EventArgs e)
        {
            
        }

        void OnMapMouseHover(object sender, EventArgs e)
        {
            HandleMouseHover(e);
        }

        private void HandleMouseHover(EventArgs e)
        {
            
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                base.MouseUp -= OnMapMouseUp;
                base.MouseMove -= OnMapMouseMove;
                base.MouseDown -= OnMapMouseDown;
                base.MouseClick -= OnMapMouseClick;
                base.MouseDoubleClick -= OnMapMouseDoubleClick;
                base.MouseHover -= OnMapMouseHover;
                base.MouseEnter -= OnMouseEnter;

                renderWorker.DoWork -= renderWorker_DoWork;
                renderWorker.RunWorkerCompleted -= renderWorker_RunWorkerCompleted;

                _renderSvc.Dispose();
                _renderSvc = null;

                _resSvc.Dispose();
                _resSvc = null;

                _selection.Dispose();
                _selection = null;

                _mapCs.Dispose();
                _selection = null;

                _agfRW.Dispose();
                _agfRW = null;

                _wktRW.Dispose();
                _wktRW = null;

                _geomFact.Dispose();
                _geomFact = null;

                _mapMeasure.Dispose();
                _mapMeasure = null;
            }
            base.Dispose(disposing);
        }

        public MgCoordinateSystem CoordinateSystem { get { return _mapCs; } }

        protected override void OnPaint(PaintEventArgs e)
        {
            //FIXME: We should maintain selection and map images separately.
            //Unfortunately my first attempt was fubar. Selections did not
            //update on change of zoom and box selections did not render. So
            //in the meantime both map and selection is in the same image,
            //meaning a full re-render on selections (ugh!)

            base.OnPaint(e);

            if (!translate.IsEmpty)
                e.Graphics.TranslateTransform(translate.X, translate.Y);
            
            if (_mapImage != null)
                e.Graphics.DrawImage(_mapImage, new PointF(0, 0));

            //Thread.Sleep(100);
            //if (_selectionImage != null)
            //    e.Graphics.DrawImage(_selectionImage, new PointF(0, 0));

            if (isDragging && (this.ActiveTool == MapActiveTool.Select || this.ActiveTool == MapActiveTool.ZoomIn))
            {
                DrawDragRectangle(e);
            }
            else
            {
                if (this.DigitizingType != MapDigitizationType.None)
                {
                    if (this.DigitizingType == MapDigitizationType.Point)
                    {
                        DrawTrackingTooltip(e, "Click to finish");
                    }
                    else
                    {
                        if (!dPtStart.IsEmpty)
                        {
                            switch (this.DigitizingType)
                            {
                                case MapDigitizationType.Circle:
                                    DrawTracingCircle(e);
                                    break;
                                case MapDigitizationType.Line:
                                    DrawTracingLine(e);
                                    break;
                                case MapDigitizationType.Rectangle:
                                    DrawTracingRectangle(e);
                                    break;
                            }
                        }
                        else if (dPath.Count > 0)
                        {
                            switch (this.DigitizingType)
                            {
                                case MapDigitizationType.LineString:
                                    DrawTracingLineString(e);
                                    break;
                                case MapDigitizationType.Polygon:
                                    DrawTracingPolygon(e);
                                    break;
                            }
                        }
                    }
                }
                else //None
                {
                    if (this.ActiveTool != MapActiveTool.None)
                    {
                        if (!string.IsNullOrEmpty(_activeTooltipText))
                            DrawTrackingTooltip(e, _activeTooltipText);
                    }
                }
            }
        }

        private static Pen CreateOutlinePen()
        {
            return new Pen(Brushes.Red, 2.0f);
        }

        private static Brush CreateFillBrush()
        {
            return new SolidBrush(Color.FromArgb(100, Color.White));
        }

        private static double GetDistanceBetween(PointF a, PointF b)
        {
            return (Math.Sqrt(Math.Pow(Math.Abs(a.X - b.X), 2) + Math.Pow(Math.Abs(a.Y - b.Y), 2)));
        }

        private void DrawTrackingTooltip(PaintEventArgs e, string text)
        {
            if (string.IsNullOrEmpty(text)) //Nothing to draw
                return;

            var f = Control.DefaultFont;
            
            /*
            var actualText = text.Replace("\\n", "\n");
            var size = e.Graphics.MeasureString(actualText, f);
            e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.LightYellow)), new Rectangle(_mouseX, _mouseY, (int)size.Width + 10, (int)size.Height + 4));
            e.Graphics.DrawString(text, f, Brushes.Black, new PointF(_mouseX + 5.0f, _mouseY + 2.0f));
             */
            
            int height = 0;
            int width = 0;
            string [] tokens = text.Split(new string[] {"\\n", "\\r\\n", "\n", Environment.NewLine }, StringSplitOptions.None);
            foreach(string t in tokens)
            {
                var size = e.Graphics.MeasureString(t, f);
                height += (int)size.Height;

                width = Math.Max(width, (int)size.Width);
            }

            e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.LightYellow)), new Rectangle(_mouseX, _mouseY, width + 10, height + 4));
            float y = 2.0f;
            float heightPerLine = height / tokens.Length;
            foreach (string t in tokens)
            {
                e.Graphics.DrawString(t, f, Brushes.Black, new PointF(_mouseX + 5.0f, _mouseY + y));
                y += heightPerLine;
            }
        }

        private void DrawTracingCircle(PaintEventArgs e)
        {
            var pt2 = new Point(dPtStart.X, dPtStart.Y);
            var diameter = (float)GetDistanceBetween(dPtStart, new PointF(_mouseX, _mouseY)) * 2.0f;
            //Trace.TraceInformation("Diameter ({0}, {1} -> {2}, {3}): {4}", dPtStart.X, dPtStart.Y, _mouseX, _mouseY, diameter);
            pt2.Offset((int)-(diameter / 2), (int)-(diameter / 2));
            e.Graphics.DrawEllipse(CreateOutlinePen(), pt2.X, pt2.Y, diameter, diameter);
            e.Graphics.FillEllipse(CreateFillBrush(), pt2.X, pt2.Y, diameter, diameter);

            DrawTrackingTooltip(e, "Click to finish");
        }

        private void DrawTracingLine(PaintEventArgs e)
        {
            e.Graphics.DrawLine(CreateOutlinePen(), dPtStart, new Point(_mouseX, _mouseY));

            DrawTrackingTooltip(e, "Click to finish");
        }

        private void DrawTracingLineString(PaintEventArgs e)
        {
            //Not enough points to constitute a line string or polygon
            if (dPath.Count < 2)
                return;
            
            e.Graphics.DrawLines(CreateOutlinePen(), dPath.ToArray());

            DrawTrackingTooltip(e, "Click again to add a new vertex. Double-click to finish");
        }

        private void DrawTracingPolygon(PaintEventArgs e)
        {
            //Not enough points to constitute a line string or polygon
            if (dPath.Count < 2)
                return;

            e.Graphics.DrawPolygon(CreateOutlinePen(), dPath.ToArray());
            e.Graphics.FillPolygon(CreateFillBrush(), dPath.ToArray());

            DrawTrackingTooltip(e, "Click again to add a new vertex. Double-click to finish");
        }

        private void DrawTracingRectangle(PaintEventArgs e)
        {
            DrawDragRectangle(e);
            DrawTrackingTooltip(e, "Click to finish");
        }

        private void DrawDragRectangle(PaintEventArgs e)
        {
            var rect = GetRectangle(dragStart, new Point(_mouseX, _mouseY));
            if (rect.HasValue)
            {
                var r = rect.Value;
                Trace.TraceInformation("Draw rangle ({0} {1}, {2} {3})", r.Left, r.Top, r.Right, r.Bottom);
                e.Graphics.DrawRectangle(CreateOutlinePen(), r);
                Trace.TraceInformation("Fill rangle ({0} {1}, {2} {3})", r.Left, r.Top, r.Right, r.Bottom);
                e.Graphics.FillRectangle(CreateFillBrush(), r);
            }
        }

        private bool _featTooltipsEnabled;

        public bool FeatureTooltipsEnabled
        {
            get { return _featTooltipsEnabled; }
            set
            {
                if (value.Equals(_featTooltipsEnabled))
                    return;

                _featTooltipsEnabled = value;
                if (!value)
                {
                    _activeTooltipText = null;
                    Invalidate();
                }

                OnPropertyChanged("FeatureTooltipsEnabled");
            }
        }

        /// <summary>
        /// Internally determines whether a tooltip query can be executed. Must be true along
        /// with <see cref="FeatureTooltipsEnabled"/> in order for a tooltip query to be executed
        /// </summary>
        internal bool TooltipsEnabled
        {
            get;
            set;
        }

        #region Digitization

        /*
         * Digitization behaviour with respect to mouse and paint events
         * 
         * Point:
         *  MouseClick -> Invoke Callback
         * 
         * Rectangle:
         *  MouseClick -> set start, temp end
         *  MouseMove -> update temp end
         *  OnPaint -> Draw rectangle from start/temp end
         *  MouseClick -> set end -> Invoke Callback
         * 
         * Line:
         *  MouseClick -> set start, temp end
         *  MouseMove -> update temp end
         *  OnPaint -> Draw line from start/temp end
         *  MouseClick -> set end -> Invoke Callback
         * 
         * LineString:
         *  MouseClick -> append point to path
         *  MouseMove -> update temp end
         *  OnPaint -> Draw line with points in path + temp end
         *  MouseDoubleClick -> append point to path -> Invoke Callback
         * 
         * Polygon:
         *  MouseClick -> append point to path
         *  MouseMove -> update temp end
         *  OnPaint -> Draw polygon fill with points in path + temp end
         *  MouseDoubleClick -> append point to path -> Invoke Callback
         * 
         * Circle:
         *  MouseClick -> set start, temp end
         *  MouseMove -> update temp end
         *  OnPaint -> Draw circle from start with radius = (dist from start to temp end)
         *  MouseClick -> set end -> Invoke Callback
         */

        private Point dPtStart; //Rectangle, Line, Circle
        private Point dPtEnd; //Rectangle, Line, Circle
        private List<Point> dPath = new List<Point>(); //LineString, Polygon

        private Delegate _digitzationCallback;

        private bool _digitizationYetToStart = true;

        public void DigitizeCircle(CircleDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.Circle;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        public void DigitizeLine(LineDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.Line;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        public void DigitizePoint(PointDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.Point;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        public void DigitizePolygon(PolygonDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.Polygon;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        public void DigitizeLineString(LineStringDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.LineString;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        public void DigitizeRectangle(RectangleDigitizationCallback callback)
        {
            this.DigitizingType = MapDigitizationType.Rectangle;
            _digitzationCallback = callback;
            _digitizationYetToStart = true;
        }

        private void ResetDigitzationState()
        {
            _digitzationCallback = null;
            dPath.Clear();
            dPtEnd.X = dPtStart.Y = 0;
            dPtStart.X = dPtStart.Y = 0;
            this.DigitizingType = MapDigitizationType.None;
            Invalidate();
        }

        private void OnCircleDigitized(Point ptStart, Point ptEnd)
        {
            var mapPt = ScreenToMapUnits(ptStart.X, ptStart.Y);
            var mapEnd = ScreenToMapUnits(ptEnd.X, ptEnd.Y);

            var radius = 0.0;
            if (_mapCs.Type == MgCoordinateSystemType.Geographic)
                radius = _mapCs.MeasureGreatCircleDistance(mapPt.X, mapPt.Y, mapEnd.X, mapEnd.Y);
            else if (_mapCs.Type == MgCoordinateSystemType.Projected)
                radius = _mapCs.MeasureEuclideanDistance(mapPt.X, mapPt.Y, mapEnd.X, mapEnd.Y);
            else
                radius = Math.Sqrt(Math.Pow(mapEnd.X - mapPt.X, 2) + Math.Pow(mapEnd.Y - mapPt.Y, 2));

            var cb = (CircleDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(mapPt.X, mapPt.Y, radius);
        }

        private void OnPolygonDigitized(List<Point> path)
        {
            double[,] coords = new double[path.Count, 2];
            for (int i = 0; i < path.Count; i++)
            {
                var pt = ScreenToMapUnits(path[i].X, path[i].Y);
                coords[i, 0] = pt.X;
                coords[i, 1] = pt.Y;
            }

            var cb = (PolygonDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(coords);
        }

        private void OnLineStringDigitized(List<Point> path)
        {
            double[,] coords = new double[path.Count, 2];
            for (int i = 0; i < path.Count; i++)
            {
                var pt = ScreenToMapUnits(path[i].X, path[i].Y);
                coords[i, 0] = pt.X;
                coords[i, 1] = pt.Y;
            }

            var cb = (LineStringDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(coords);
        }

        private void OnLineDigitized(Point start, Point end)
        {
            var mapStart = ScreenToMapUnits(start.X, start.Y);
            var mapEnd = ScreenToMapUnits(end.X, end.Y);

            var cb = (LineDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(mapStart.X, mapStart.Y, mapEnd.X, mapEnd.Y);
        }

        private void OnRectangleDigitized(Rectangle rect)
        {
            var mapLL = ScreenToMapUnits(rect.Left, rect.Bottom);
            var mapUR = ScreenToMapUnits(rect.Right, rect.Top);

            var cb = (RectangleDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(mapLL.X, mapLL.Y, mapUR.X, mapUR.Y);
        }

        private void OnPointDigitizationCompleted(Point p)
        {
            var mapPt = ScreenToMapUnits(p.X, p.Y);
            var cb = (PointDigitizationCallback)_digitzationCallback;
            ResetDigitzationState();
            cb(mapPt.X, mapPt.Y);
        }

        #endregion

        public void Init(MgdMap map)
        {
            _agfRW = new MgAgfReaderWriter();
            _wktRW = new MgWktReaderWriter();
            _geomFact = new MgGeometryFactory();
            
            var csFact = new MgCoordinateSystemFactory();
            _mapCs = csFact.Create(map.GetMapSRS());
            _mapMeasure = _mapCs.GetMeasure();
            _resSvc = MgServiceFactory.CreateResourceService();
            _renderSvc = MgServiceFactory.CreateRenderingService();
            _overlayRenderOpts = new MgRenderingOptions(MgImageFormats.Png, (1 | 2 | 4), new MgColor(0, 0, 255));
            //_selectionRenderOpts = new MgRenderingOptions(MgImageFormats.Png, (1 | 4), new MgColor(0, 0, 255));

            _map = map;
            var bgColor = _map.GetBackgroundColor();
            if (bgColor.Length == 8 || bgColor.Length == 6)
            {
                _mapBgColor = ColorTranslator.FromHtml("#" + bgColor);
                this.BackColor = _mapBgColor;
            }
            _map.SetDisplaySize(this.Width, this.Height);
            _selection = new MgdSelection(_map);

            if (firstRun)
            {
                this.Resize += new EventHandler(OnControlResized);
                firstRun = false;
            }

            var env = _map.GetMapExtent();
            var ll = env.LowerLeftCoordinate;
            var ur = env.UpperRightCoordinate;

            _extX1 = _orgX1 = ll.X;
            _extY2 = _orgY2 = ll.Y;
            _extX2 = _orgX2 = ur.X;
            _extY1 = _orgY1 = ur.Y;

            if ((_orgX1 - _orgX2) == 0 || (_orgY1 - _orgY2) == 0)
            {
                _extX1 = _orgX1 = -.1;
                _extY2 = _orgX2 = .1;
                _extX2 = _orgY1 = -.1;
                _extY1 = _orgY2 = .1;
            }

            RebuildLayerInfoCache();
            CacheGeometryProperties(_map.GetLayers());

            double scale = CalculateScale(Math.Abs(_orgX2 - _orgX1), Math.Abs(_orgY2 - _orgY1), this.Width, this.Height);
            _map.SetViewCenterXY(_extX1 + (_extX2 - _extX1) / 2, _extY2 + (_extY1 - _extY2) / 2);
            _map.SetViewScale(scale);

#if VIEWER_DEBUG
            CreateDebugFeatureSource();
#endif
            this.Focus();

            var handler = this.ViewerInitialized;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

        private void RebuildLayerInfoCache()
        {
            _cachedLayerDefinitions.Clear();
            _tooltipExpressions.Clear();
            _propertyMappings.Clear();
            //Pre-cache layer definitions and tooltip properties
            var layers = _map.GetLayers();
            MgStringCollection resIds = new MgStringCollection();
            for (int i = 0; i < layers.GetCount(); i++)
            {
                var layer = layers.GetItem(i);
                var ldf = layer.GetLayerDefinition();
                resIds.Add(ldf.ToString());
            }
            MgStringCollection contents = _resSvc.GetResourceContents(resIds, null);
            for (int i = 0; i < contents.GetCount(); i++)
            {
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(contents.GetItem(i));

                _cachedLayerDefinitions[resIds.GetItem(i)] = doc;
                XmlNodeList nodes = doc.GetElementsByTagName("ToolTip");
                if (nodes.Count == 1)
                    _tooltipExpressions[resIds.GetItem(i)] = nodes[0].InnerText;

                XmlNodeList propMaps = doc.GetElementsByTagName("PropertyMapping");
                if (propMaps.Count > 0)
                {
                    NameValueCollection propertyMappings = new NameValueCollection();
                    foreach (XmlNode pm in propMaps)
                    {
                        propertyMappings[pm["Name"].InnerText] = pm["Value"].InnerText;
                    }
                    _propertyMappings[resIds.GetItem(i)] = propertyMappings;
                }
            }
        }

        private double CalculateScale(double mcsW, double mcsH, int devW, int devH)
        {
            var mpu = _map.GetMetersPerUnit();
            var mpp = 0.0254 / _map.DisplayDpi;
            if (devH * mcsW > devW * mcsH)
                return mcsW * mpu / (devW * mpp); //width-limited
            else
                return mcsH * mpu / (devH * mpp); //height-limited
        }

        public event EventHandler ViewerInitialized;

        void OnControlResized(object sender, EventArgs e)
        {
            _map.SetDisplaySize(this.Width, this.Height);
            RefreshMap(false);
        }

        public void ClearSelection()
        {
            _selection.Clear();
            
            var handler = this.SelectionChanged;
            if (handler != null)
                handler(this, EventArgs.Empty);

            RefreshMap(true);
        }

        public MgdMap GetMap()
        {
            return _map;
        }

        public MgdSelection GetSelection()
        {
            return _selection;
        }

        private MapDigitizationType _digitizingType = MapDigitizationType.None;

        public MapDigitizationType DigitizingType
        {
            get { return _digitizingType; }
            private set
            {
                if (_digitizingType.Equals(value))
                    return;

                if (value != MapDigitizationType.None)
                {
                    this.ActiveTool = MapActiveTool.None;
                    this.Cursor = Cursors.Cross;
                }
                else
                {
                    this.Cursor = Cursors.Default;
                }

                _digitizingType = value;

                OnPropertyChanged("DigitizingType");
            }
        }

        class RenderWorkArgs
        {
            //public MgRenderingOptions SelectionRenderingOptions { get; set; }

            public MgRenderingOptions MapRenderingOptions { get; set; }

            public bool RaiseEvents { get; set; }
        }

        class RenderResult
        {
            public Image Image { get; set; }

            //public Image SelectionImage { get; set; }

            public bool RaiseEvents { get; set; }
        }

        public void RefreshMap()
        {
            RefreshMap(true);
        }

        /*
        internal void RenderSelection()
        {
            //TODO: Queue the refresh map request
            if (this.IsBusy)
                return;

            if (_selectionImage != null)
            {
                _selectionImage.Dispose();
                _selectionImage = null;
            }

            this.IsBusy = true;
            renderWorker.RunWorkerAsync(new RenderWorkArgs() 
            { 
                SelectionRenderingOptions = _selectionRenderOpts, 
                RaiseEvents = false, 
            });
        }*/

        internal void RefreshMap(bool raiseEvents)
        {
            //TODO: Queue the refresh map request
            if (this.IsBusy)
                return;

            if (this.Image != null)
            {
                this.Image.Dispose();
                this.Image = null;
            }
            /*
            if (_selectionImage != null)
            {
                _selectionImage.Dispose();
                _selectionImage = null;
            }*/

            this.IsBusy = true;
            renderWorker.RunWorkerAsync(new RenderWorkArgs() 
            { 
                MapRenderingOptions = _overlayRenderOpts, 
                //SelectionRenderingOptions = _selectionRenderOpts, 
                RaiseEvents = raiseEvents
            });
        }

        public event EventHandler MapRefreshed;

        private bool _busy = false;

#if TRACE
        private Stopwatch _renderSw = new Stopwatch();
#endif

        /// <summary>
        /// Indicates whether a rendering operation is in progress
        /// </summary>
        public bool IsBusy
        {
            get { return _busy; }
            private set
            {
                if (_busy.Equals(value))
                    return;

                _busy = value;
#if TRACE
                Trace.TraceInformation("IsBusy = " + _busy);
                if (value)
                {
                    _renderSw.Reset();
                    _renderSw.Start();
                }
                else
                {
                    _renderSw.Stop();
                    Trace.TraceInformation("Rendering operation took {0}ms", _renderSw.ElapsedMilliseconds);
                }
#endif
                OnPropertyChanged("IsBusy");
            }
        }

        public void PanLeft(bool refresh)
        {
            PanTo(_extX1 + (_extX2 - _extX1) / 3, _extY2 + (_extY1 - _extY2) / 2, refresh);
        }

        public void PanUp(bool refresh)
        {
            PanTo(_extX1 + (_extX2 - _extX1) / 2, _extY1 - (_extY1 - _extY2) / 3, refresh);
        }

        public void PanRight(bool refresh)
        {
            PanTo(_extX2 - (_extX2 - _extX1) / 3, _extY2 + (_extY1 - _extY2) / 2, refresh);
        }

        public void PanDown(bool refresh)
        {
            PanTo(_extX1 + (_extX2 - _extX1) / 2, _extY2 + (_extY1 - _extY2) / 3, refresh);
        }

        public void ZoomExtents()
        {
            var scale = CalculateScale((_orgX2 - _orgX1), (_orgY1 - _orgY2), this.Width, this.Height);
            ZoomToView(_orgX1 + ((_orgX2 - _orgX1) / 2), _orgY2 + ((_orgY1 - _orgY2) / 2), scale, true);
        }

        public void ZoomToExtents(double llx, double lly, double urx, double ury)
        {
            var scale = CalculateScale((urx - llx), (ury - lly), this.Width, this.Height);
            ZoomToView(llx + ((urx - llx) / 2), ury + ((lly - ury) / 2), scale, true);
        }

        public void ZoomToScale(double scale)
        {
            ZoomToView(_extX1 + (_extX2 - _extX1) / 2, _extY2 + (_extY1 - _extY2) / 2, scale, true);
        }

        public void ZoomToView(double x, double y, double scale, bool refresh)
        {
            ZoomToView(x, y, scale, refresh, true);
        }

        internal void PanTo(double x, double y, bool refresh)
        {
            ZoomToView(x, y, _map.ViewScale, refresh);
        }

        internal void ZoomToView(double x, double y, double scale, bool refresh, bool raiseEvents)
        {
            _map.SetViewCenterXY(x, y);
#if VIEWER_DEBUG
            UpdateCenterDebugPoint();
            //var mapExt = _map.MapExtent;
            //var dataExt = _map.DataExtent;
            Trace.TraceInformation("Map Extent is ({0},{1} {2},{3})", mapExt.LowerLeftCoordinate.X, mapExt.LowerLeftCoordinate.Y, mapExt.UpperRightCoordinate.X, mapExt.UpperRightCoordinate.Y);
            Trace.TraceInformation("Data Extent is ({0},{1} {2},{3})", dataExt.LowerLeftCoordinate.X, dataExt.LowerLeftCoordinate.Y, dataExt.UpperRightCoordinate.X, dataExt.UpperRightCoordinate.Y);

            Trace.TraceInformation("Center is (" + x + ", " + y + ")");
#endif
            var oldScale = _map.ViewScale;
            _map.SetViewScale(Math.Max(scale, MINIMUM_ZOOM_SCALE));

            if (oldScale != _map.ViewScale)
            {
                var handler = this.MapScaleChanged;
                if (handler != null)
                    handler(this, EventArgs.Empty);
            }

            //Update current extents
            double mpu = _map.GetMetersPerUnit();
            double mpp = 0.0254 / _map.DisplayDpi;

            var mcsWidth = _map.DisplayWidth * mpp * scale / mpu;
            var mcsHeight = _map.DisplayHeight * mpp * scale / mpu;

            _extX1 = x - mcsWidth / 2;
            _extY1 = y + mcsHeight / 2;
            _extX2 = x + mcsWidth / 2;
            _extY2 = y - mcsHeight / 2;

#if VIEWER_DEBUG
            Trace.TraceInformation("Current extents is ({0},{1} {2},{3})", _extX1, _extY1, _extX2, _extY2);
#endif

            //Then refresh
            if (refresh)
                RefreshMap(raiseEvents);
        }

        public event EventHandler MapScaleChanged;

        public event EventHandler SelectionChanged;

        private void renderWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            var args = (RenderWorkArgs)e.Argument;
            var res = new RenderResult() { RaiseEvents = args.RaiseEvents };
            if (args.MapRenderingOptions != null)
            {
                var br = _renderSvc.RenderDynamicOverlay(_map, _selection, args.MapRenderingOptions);
                byte[] b = new byte[br.GetLength()];
                br.Read(b, b.Length);
                using (var ms = new MemoryStream(b))
                {
                    res.Image = Image.FromStream(ms);
                }
            }
            //else if (args.SelectionRenderingOptions != null)
            //{
            //    var br = _renderSvc.RenderDynamicOverlay(_map, _selection, args.SelectionRenderingOptions);
            //    byte[] b = new byte[br.GetLength()];
            //    br.Read(b, b.Length);
            //    using (var ms = new MemoryStream(b))
            //    {
            //        res.SelectionImage = Image.FromStream(ms);
            //    }
            //}

            e.Result = res;
        }

        private void renderWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            this.IsBusy = AreWorkersBusy();
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message, "Error");
            }
            else
            {
                var res = (RenderResult)e.Result;
                //reset translation
                translate = new System.Drawing.Point();

                //set the image
                //if (res.SelectionImage != null)
                //{
                //    _selectionImage = res.SelectionImage;
                //    Invalidate();
                //}
                if (res.Image != null)
                {
                    this.Image = res.Image;
                }
                /*
                var center = _map.ViewCenter;
                var ext = _map.DataExtent;
                System.Diagnostics.Trace.TraceInformation(
                    "**POST-RENDER**{2}Map Center: {0}, {1}{2}Lower left: {3}, {4}{2}Upper Right: {5}, {6}",
                    center.Coordinate.X,
                    center.Coordinate.Y,
                    Environment.NewLine,
                    ext.LowerLeftCoordinate.X,
                    ext.LowerLeftCoordinate.Y,
                    ext.UpperRightCoordinate.X,
                    ext.UpperRightCoordinate.Y);
                */
                if (res.RaiseEvents)
                {
                    var handler = this.MapRefreshed;
                    if (handler != null)
                        handler(this, EventArgs.Empty);
                }
            }
        }

        private bool AreWorkersBusy()
        {
            return renderWorker.IsBusy;
        }

        public void InitialMapView()
        {
            var scale = CalculateScale((_orgX2 - _orgX1), (_orgY1 - _orgY2), this.Width, this.Height);
            ZoomToView(_orgX1 + ((_orgX2 - _orgX1) / 2), _orgY2 + ((_orgY1 - _orgY2) / 2), scale, true);
        }

        private static Rectangle? GetRectangle(Point dPtStart, Point dPtEnd)
        {
            int? left = null;
            int? right = null;
            int? top = null;
            int? bottom = null;

            if (dPtEnd.X < dPtStart.X)
            {
                if (dPtEnd.Y < dPtStart.Y)
                {
                    left = dPtEnd.X;
                    bottom = dPtStart.Y;
                    top = dPtEnd.Y;
                    right = dPtStart.X;
                }
                else if (dPtEnd.Y > dPtStart.Y)
                {
                    left = dPtEnd.X;
                    bottom = dPtEnd.Y;
                    top = dPtStart.Y;
                    right = dPtStart.X;
                }
                else
                {
                    //Equal
                }
            }
            else
            {
                if (dPtEnd.X > dPtStart.X)
                {
                    if (dPtEnd.Y < dPtStart.Y)
                    {
                        left = dPtStart.X;
                        bottom = dPtStart.Y;
                        top = dPtEnd.Y;
                        right = dPtEnd.X;
                    }
                    else if (dPtEnd.Y > dPtStart.Y)
                    {
                        left = dPtStart.X;
                        bottom = dPtEnd.Y;
                        top = dPtStart.Y;
                        right = dPtEnd.X;
                    }
                    else
                    {
                        //Equal
                    }
                }
                //else equal
            }
            if (left.HasValue && right.HasValue && top.HasValue && bottom.HasValue)
            {
                return new Rectangle(left.Value, top.Value, (right.Value - left.Value), (bottom.Value - top.Value));
            }
            return null;
        }

        private double _zoomInFactor;
        private double _zoomOutFactor;

        public double ZoomInFactor
        {
            get { return _zoomInFactor; }
            set
            {
                if (value.Equals(_zoomInFactor))
                    return;
                _zoomInFactor = value;
                OnPropertyChanged("ZoomInFactor");
            }
        }

        public double ZoomOutFactor
        {
            get { return _zoomOutFactor; }
            set
            {
                if (value.Equals(_zoomOutFactor))
                    return;
                _zoomOutFactor = value;
                OnPropertyChanged("ZoomOutFactor");
            }
        }

        private static string MakeWktPolygon(double x1, double y1, double x2, double y2)
        {
            return "POLYGON((" + x1 + " " + y1 + ", " + x2 + " " + y1 + ", " + x2 + " " + y2 + ", " + x1 + " " + y2 + ", " + x1 + " " + y1 + "))";
        }

        private Dictionary<string, NameValueCollection> _propertyMappings = new Dictionary<string, NameValueCollection>();

        internal Dictionary<string, NameValueCollection> AllPropertyMappings { get { return _propertyMappings; } }

        private Dictionary<string, XmlDocument> _cachedLayerDefinitions = new Dictionary<string, XmlDocument>();
        private Dictionary<string, string> _tooltipExpressions = new Dictionary<string, string>();

        private int? _lastTooltipX;
        private int? _lastTooltipY;

        private string QueryFirstVisibleTooltip(int x, int y)
        {
            //No intialized map
            if (_map == null)
                return "";

            //No change in position
            if (_lastTooltipX == x && _lastTooltipY == y && !string.IsNullOrEmpty(_activeTooltipText))
                return _activeTooltipText;

            if (_lastTooltipX.HasValue && _lastTooltipY.HasValue)
            {
                //Not considered a significant change
                if (Math.Abs(x - _lastTooltipX.Value) < MOUSE_MOVE_TOLERANCE ||
                    Math.Abs(y - _lastTooltipY.Value) < MOUSE_MOVE_TOLERANCE)
                    return _activeTooltipText;
            }

            _lastTooltipX = x;
            _lastTooltipY = y;

            var layers = _map.GetLayers();
            
            var mapPt1 = ScreenToMapUnits(x - 2, y - 2);
            var mapPt2 = ScreenToMapUnits(x + 2, y + 2);
            var ringCoords = new MgCoordinateCollection();
            ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt2.X, mapPt2.Y));
            ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt1.X, mapPt2.Y));
            ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt1.X, mapPt1.Y));
            ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt2.X, mapPt1.Y));
            ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt2.X, mapPt2.Y)); //Close it
            var poly = _geomFact.CreatePolygon(_geomFact.CreateLinearRing(ringCoords), new MgLinearRingCollection());

            for (int i = 0; i < layers.GetCount(); i++)
            {
                var layer = (MgdLayer)layers.GetItem(i);
                var layerGroup = layer.GetGroup();
                //Layer not visible or its parent is not visible
                if (!layer.IsVisible() || (layerGroup != null && !layerGroup.IsVisible()))
                    continue;

                //No defined tooltips
                if (!layer.HasTooltips())
                    continue;

                //Drawing layers have no intelligence
                string fsId = layer.FeatureSourceId;
                if (fsId.EndsWith(MgResourceType.DrawingSource))
                    continue;

                //Nor do rasters
                if (IsRasterLayer(layer))
                    continue;

                var objId = layer.GetObjectId();
                var ldfId = layer.GetLayerDefinition();
                var ldfIdStr = ldfId.ToString();

                //No tooltip detected
                if (!_tooltipExpressions.ContainsKey(ldfIdStr))
                    continue;

                string propName = "QUERYTOOLTIP";
                MgFeatureQueryOptions query = new MgFeatureQueryOptions();
                query.AddComputedProperty(propName, _tooltipExpressions[ldfId.ToString()]);
                query.SetSpatialFilter(_layerGeomProps[objId], poly, MgFeatureSpatialOperations.Intersects);

                MgFeatureReader reader = null;
                reader = layer.SelectFeatures(query);
                try
                {
                    if (reader.ReadNext())
                    {
                        object value = null;

                        var pt = reader.GetPropertyType(propName);
                        switch (pt)
                        {
                            case MgPropertyType.String:
                                value = reader.GetString(propName);
                                break;
                            case MgPropertyType.Boolean:
                                value = reader.GetBoolean(propName);
                                break;
                            case MgPropertyType.Byte:
                                value = reader.GetByte(propName);
                                break;
                            case MgPropertyType.DateTime:
                                value = reader.GetByte(propName);
                                break;
                            case MgPropertyType.Double:
                            case MgPropertyType.Decimal:
                                value = reader.GetDouble(propName);
                                break;
                            case MgPropertyType.Int16:
                                value = reader.GetInt16(propName);
                                break;
                            case MgPropertyType.Int32:
                                value = reader.GetInt32(propName);
                                break;
                            case MgPropertyType.Int64:
                                value = reader.GetInt64(propName);
                                break;
                            case MgPropertyType.Single:
                                value = reader.GetSingle(propName);
                                break;
                        }

                        if (value != null)
                            return value.ToString(); //.Replace("\n", Environment.NewLine);
                    }
                }
                finally
                {
                    reader.Close();
                }
            }

            return string.Empty;
        }

        private static bool IsRasterClass(MgClassDefinition cls)
        {
            var props = cls.GetProperties();
            for (int i = 0; i < props.GetCount(); i++)
            {
                var p = props.GetItem(i);
                if (p.PropertyType == MgFeaturePropertyType.RasterProperty)
                    return true;
            }
            return false;
        }

        private static bool IsRasterLayer(MgLayerBase layer)
        {
            var cls = layer.GetClassDefinition();

            return IsRasterClass(cls);
        }

        private Dictionary<string, string> _layerGeomProps = new Dictionary<string, string>();

        private void SelectByGeometry(MgGeometry geom)
        {
#if TRACE
            var sw = new Stopwatch();
            sw.Start();
#endif

            var layers = _map.GetLayers();

            _selection.Clear();
            _selectionImage = null;

            for (int i = 0; i < layers.GetCount(); i++)
            {
                var layer = layers.GetItem(i);
                if (!layer.Selectable || !layer.IsVisible())
                    continue;

                string fsId = layer.FeatureSourceId;
                if (fsId.EndsWith(MgResourceType.DrawingSource))
                    continue;

                //Nor do rasters
                if (IsRasterLayer(layer))
                    continue;

                //This could be a newly added layer
                CheckAndCacheGeometryProperty(layer);

                var objId = layer.GetObjectId();
                MgFeatureQueryOptions query = new MgFeatureQueryOptions();
                query.SetSpatialFilter(_layerGeomProps[objId], geom, MgFeatureSpatialOperations.Intersects);

                MgFeatureReader reader = layer.SelectFeatures(query);
                _selection.AddFeatures(layer, reader, 0);
            }

#if TRACE
            sw.Stop();
            Trace.TraceInformation("Selection processing completed in {0}ms", sw.ElapsedMilliseconds);
#endif

            var handler = this.SelectionChanged;
            if (handler != null)
                handler(this, EventArgs.Empty);

            string xml = _selection.ToXml();
            if (!string.IsNullOrEmpty(xml))
                RefreshMap();
                //RenderSelection();
        }

        private void CacheGeometryProperties(MgLayerCollection layers)
        {
            //Cache geometry properties
            for (int i = 0; i < layers.GetCount(); i++)
            {
                var layer = layers.GetItem(i);
                if (!layer.Selectable || !layer.IsVisible())
                    continue;

                var objId = layer.GetObjectId();
                if (_layerGeomProps.ContainsKey(objId))
                    continue;

                string fsId = layer.FeatureSourceId;
                if (fsId.EndsWith(MgResourceType.DrawingSource))
                    continue;

                CheckAndCacheGeometryProperty(layer);
            }
        }

        private void CheckAndCacheGeometryProperty(MgLayerBase layer)
        {
            var objId = layer.GetObjectId();
            if (!_layerGeomProps.ContainsKey(objId))
            {
                var cls = layer.GetClassDefinition();
                var geomName = cls.DefaultGeometryPropertyName;
                if (!string.IsNullOrEmpty(geomName))
                {
                    _layerGeomProps[objId] = geomName;
                }
            }
        }

        #region Mouse handlers

        private void OnMapMouseDown(object sender, MouseEventArgs e)
        {
            HandleMouseDown(e);
        }

        private void OnMapMouseMove(object sender, MouseEventArgs e)
        {
            HandleMouseMove(e);
        }
        
        private void OnMapMouseUp(object sender, MouseEventArgs e)
        {
            HandleMouseUp(e);
        }

        private void OnMapMouseClick(object sender, MouseEventArgs e)
        {
            HandleMouseClick(e);
        }

        private void OnMapMouseDoubleClick(object sender, MouseEventArgs e)
        {
            HandleMouseDoubleClick(e);
        }

        private void HandleMouseDoubleClick(MouseEventArgs e)
        {
            //Not enough points to constitute a line string or polygon
            if (dPath.Count < 2)
                return;

            if (this.DigitizingType == MapDigitizationType.LineString)
            {
                //Fix the last one, can't edit last one because points are value types
                dPath.RemoveAt(dPath.Count - 1);
                dPath.Add(new Point(e.X, e.Y));
                OnLineStringDigitized(dPath);
            }
            else if (this.DigitizingType == MapDigitizationType.Polygon)
            {
                //Fix the last one, can't edit last one because points are value types
                dPath.RemoveAt(dPath.Count - 1);
                dPath.Add(new Point(e.X, e.Y));
                OnPolygonDigitized(dPath);
            }
        }

        private void HandleMouseClick(MouseEventArgs e)
        {
            if (this.DigitizingType != MapDigitizationType.None)
            {
                //Points are easy, one click and you're done
                if (this.DigitizingType == MapDigitizationType.Point)
                {
                    OnPointDigitizationCompleted(new Point(e.X, e.Y));
                }
                else
                {
                    //Check first click in digitization
                    if (_digitizationYetToStart)
                    {
                        if (this.DigitizingType == MapDigitizationType.LineString ||
                            this.DigitizingType == MapDigitizationType.Polygon)
                        {
                            dPath.Add(new Point(e.X, e.Y));
                            dPath.Add(new Point(e.X, e.Y)); //This is a transient one
                        }
                        else
                        {
                            dPtStart.X = e.X;
                            dPtStart.Y = e.Y;
                        }
                        _digitizationYetToStart = false;
                    }
                    else
                    {
                        if (this.DigitizingType == MapDigitizationType.LineString ||
                            this.DigitizingType == MapDigitizationType.Polygon)
                        {
                            var pt = dPath[dPath.Count - 1];
                            pt.X = e.X;
                            pt.Y = e.Y;
                            dPath.Add(new Point(e.X, e.Y)); //This is a transient one
                        }
                        else
                        {
                            //Fortunately, these are all 2-click affairs meaning this is
                            //the second click
                            switch (this.DigitizingType)
                            {
                                case MapDigitizationType.Circle:
                                    {
                                        dPtEnd.X = e.X;
                                        dPtEnd.Y = e.Y;
                                        OnCircleDigitized(dPtStart, dPtEnd);
                                    }
                                    break;
                                case MapDigitizationType.Line:
                                    {
                                        dPtEnd.X = e.X;
                                        dPtEnd.Y = e.Y;
                                        OnLineDigitized(dPtStart, dPtEnd);
                                    }
                                    break;
                                case MapDigitizationType.Rectangle:
                                    {
                                        dPtEnd.X = e.X;
                                        dPtEnd.Y = e.Y;
                                        var rect = GetRectangle(dPtStart, dPtEnd);
                                        if (rect.HasValue)
                                            OnRectangleDigitized(rect.Value);
                                    }
                                    break;
                            }
                        }
                    }
                }
            }
            else
            {
                if (this.ActiveTool == MapActiveTool.Select)
                {
                    var mapPt1 = ScreenToMapUnits(e.X - 2, e.Y - 2);
                    var mapPt2 = ScreenToMapUnits(e.X + 2, e.Y + 2);

                    var coord1 = _geomFact.CreateCoordinateXY(mapPt1.X, mapPt1.Y);
                    var coord2 = _geomFact.CreateCoordinateXY(mapPt2.X, mapPt2.Y);

                    var dist = _mapMeasure.GetDistance(coord1, coord2);

                    MgGeometry geom = _wktRW.Read(MakeWktPolygon(mapPt1.X, mapPt1.Y, mapPt2.X, mapPt2.Y));

                    SelectByGeometry(geom);
                }
                else if (this.ActiveTool == MapActiveTool.ZoomIn)
                {
                    if (!isDragging)
                    {
                        var mapPt = ScreenToMapUnits(e.X, e.Y);
                        var scale = _map.ViewScale;
                        ZoomToView(mapPt.X, mapPt.Y, scale * ZoomInFactor, true);
                    }
                }
                else if (this.ActiveTool == MapActiveTool.ZoomOut)
                {
                    if (!isDragging)
                    {
                        var mapPt = ScreenToMapUnits(e.X, e.Y);
                        var scale = _map.ViewScale;
                        ZoomToView(mapPt.X, mapPt.Y, scale * ZoomOutFactor, true);
                    }
                }
            }
        }

        private void HandleMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                dragStart = e.Location;
                Trace.TraceInformation("Drag started at (" + dragStart.X + ", " + dragStart.Y + ")");

                switch (this.ActiveTool)
                {
                    case MapActiveTool.Pan:
                        Trace.TraceInformation("START PANNING");
                        break;
                    case MapActiveTool.Select:
                        Trace.TraceInformation("START SELECT");
                        break;
                    case MapActiveTool.ZoomIn:
                        Trace.TraceInformation("START ZOOM");
                        break;
                }
            }
        }

        private System.Drawing.Point translate;

        private System.Drawing.Point dragStart;
        bool isDragging = false;
        
        private int _mouseX;
        private int _mouseY;

        /*
        class ToolTipWaitArgs
        {
            public int Interval { get; set; }

            public int MouseX { get; set; }

            public int MouseY { get; set; }
        }

        void TooltipWaitProc(ToolTipWaitArgs e)
        {
            Thread.Sleep(e.Interval);
            this.BeginInvoke(new MethodInvoker(() =>
            {
                //Compare old position against current
                if ((Math.Abs(e.MouseX - _mouseX) < 2) &&
                    (Math.Abs(e.MouseY - _mouseY) < 2))
                {
                    FireTooltipQuery();
                }
            }));
        }

        private void FireTooltipQuery()
        {
            string tooltip = QueryFirstMatchingTooltip();
            if (tooltip != null)
            {
                _tooltip.Show(tooltip, this);
            }
            else
            {
                _tooltip.Hide(this);
            }
        }

        private string QueryFirstMatchingTooltip()
        {
            throw new NotImplementedException();
        }
        */

        private string _activeTooltipText;

        private int _mouseDx;
        private int _mouseDy;

        /// <summary>
        /// A mouse is considered to have moved if the differerence in either X or Y directions is greater
        /// than this number
        /// </summary>
        const int MOUSE_MOVE_TOLERANCE = 2;

        private void HandleMouseMove(MouseEventArgs e)
        {
            if (_mouseX == e.X &&
                _mouseY == e.Y)
            {
                return;
            }

            //Record displacement
            _mouseDx = e.X - _mouseX;
            _mouseDy = e.Y - _mouseY;

            _mouseX = e.X;
            _mouseY = e.Y;

            var mapPt = ScreenToMapUnits(e.X, e.Y);
            OnMouseMapPositionChanged(mapPt.X, mapPt.Y);

            if (this.ActiveTool == MapActiveTool.Pan || this.ActiveTool == MapActiveTool.Select || this.ActiveTool == MapActiveTool.ZoomIn)
            {
                if (e.Location != dragStart && !isDragging && e.Button == MouseButtons.Left)
                {
                    isDragging = true;
                }

                if (this.ActiveTool == MapActiveTool.Pan)
                {
                    if (isDragging)
                    {
                        translate = new System.Drawing.Point(e.X - dragStart.X, e.Y - dragStart.Y);
                    }
                }

                // FIXME: 
                //
                // We really need a JS setTimeout() equivalent for C# because that's what we want
                // to do here, set a delayed call to QueryFirstVisibleTooltip() that is aborted if
                // the mouse pointer has moved significantly since the last time.
                //
                // A timer based approach could probably work, but I haven't figured out the best 
                // way yet.

                this.TooltipsEnabled = !isDragging && this.FeatureTooltipsEnabled;

                //Only query for tooltips if not digitizing
                if (this.DigitizingType == MapDigitizationType.None &&
                   (this.ActiveTool == MapActiveTool.Select || this.ActiveTool == MapActiveTool.Pan) &&
                    this.TooltipsEnabled)
                {
#if TRACE
                    var sw = new Stopwatch();
                    sw.Start();
#endif
                    _activeTooltipText = QueryFirstVisibleTooltip(e.X, e.Y);
#if TRACE
                    sw.Stop();
                    Trace.TraceInformation("QueryFirstVisibleTooltip() executed in {0}ms", sw.ElapsedMilliseconds);
#endif
                }
                else
                {
                    _activeTooltipText = null;
                }

                if (e.Button == MouseButtons.Left || !string.IsNullOrEmpty(_activeTooltipText))
                    Invalidate();
            }
            else if (this.DigitizingType != MapDigitizationType.None)
            {
                if (dPath.Count >= 2)
                {
                    //Fix the last one, can't edit last one because points are value types
                    dPath.RemoveAt(dPath.Count - 1);
                    dPath.Add(new Point(e.X, e.Y));
                    Trace.TraceInformation("Updating last point of a {0} point path", dPath.Count);
                }
                Invalidate();
            }
        }

        private void HandleMouseUp(MouseEventArgs e)
        {
            if (isDragging)
            {
                isDragging = false;

                if (this.ActiveTool == MapActiveTool.Pan)
                {
                    //System.Diagnostics.Trace.TraceInformation("Dragged screen distance (" + translate.X + ", " + translate.Y + ")");

                    int dx = e.X - dragStart.X;
                    int dy = e.Y - dragStart.Y;

                    var centerScreen = new Point(this.Location.X + (this.Width / 2), this.Location.Y + (this.Height / 2));

                    centerScreen.X -= translate.X;
                    centerScreen.Y -= translate.Y;

                    var pt = _map.ViewCenter.Coordinate;
                    var coord = ScreenToMapUnits(centerScreen.X, centerScreen.Y);

                    double mdx = coord.X - pt.X;
                    double mdy = coord.Y - pt.Y;

                    ZoomToView(coord.X, coord.Y, _map.ViewScale, true);
                    Trace.TraceInformation("END PANNING");
                }
                else if (this.ActiveTool == MapActiveTool.Select || this.ActiveTool == MapActiveTool.ZoomIn)
                {
                    var mapPt = ScreenToMapUnits(e.X, e.Y);
                    var mapDragPt = ScreenToMapUnits(dragStart.X, dragStart.Y);
                    var ringCoords = new MgCoordinateCollection();
                    ringCoords.Add(_geomFact.CreateCoordinateXY(mapDragPt.X, mapDragPt.Y));
                    ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt.X, mapDragPt.Y));
                    ringCoords.Add(_geomFact.CreateCoordinateXY(mapPt.X, mapPt.Y));
                    ringCoords.Add(_geomFact.CreateCoordinateXY(mapDragPt.X, mapPt.Y));
                    ringCoords.Add(_geomFact.CreateCoordinateXY(mapDragPt.X, mapDragPt.Y)); //Close it
                    var poly = _geomFact.CreatePolygon(_geomFact.CreateLinearRing(ringCoords), new MgLinearRingCollection());

                    if (this.ActiveTool == MapActiveTool.Select)
                    {
                        SelectByGeometry(poly);
                    }
                    else //Zoom
                    {
                        var env = poly.Envelope();
                        var mcsW = env.Width;
                        var mcsH = env.Height;
                        var centerX = env.LowerLeftCoordinate.X + mcsW / 2;
                        var centerY = env.LowerLeftCoordinate.Y + mcsH / 2;
                        var scale = CalculateScale(mcsW * 2.0, mcsH * 2.0, this.Width, this.Height);
                        ZoomToView(centerX, centerY, scale, true, true);
                    }
                }
            }
        }

        private void OnMouseMapPositionChanged(double x, double y)
        {
            var handler = this.MouseMapPositionChanged;
            if (handler != null)
                handler(this, new MapPointEventArgs(x, y));
        }

        public event EventHandler<MapPointEventArgs> MouseMapPositionChanged;

        #endregion

        private MapActiveTool _tool;

        public MapActiveTool ActiveTool
        {
            get
            {
                return _tool;
            }
            set
            {
                if (_tool.Equals(value))
                    return;

                _tool = value;
                switch (value)
                {
                    case MapActiveTool.Pan:
                        using (var ms = new MemoryStream(Properties.Resources.grab))
                        {
                            this.Cursor = new Cursor(ms);
                        }
                        break;
                    case MapActiveTool.ZoomIn:
                        using (var ms = new MemoryStream(Properties.Resources.zoomin))
                        {
                            this.Cursor = new Cursor(ms);
                        }
                        break;
                    case MapActiveTool.ZoomOut:
                        using (var ms = new MemoryStream(Properties.Resources.zoomout))
                        {
                            this.Cursor = new Cursor(ms);
                        }
                        break;
                    case MapActiveTool.None:
                    case MapActiveTool.Select:
                        {
                            this.Cursor = Cursors.Default;
                        }
                        break;
                }

                //Clear to prevent stray tooltips from being rendered
                if (value != MapActiveTool.Select &&
                    value != MapActiveTool.Pan)
                {
                    _activeTooltipText = null;
                }

                if (value != MapActiveTool.None)
                    this.DigitizingType = MapDigitizationType.None;

                OnPropertyChanged("ActiveTool");
            }
        }

        public event EventHandler MapActiveToolChanged;

        public PointF ScreenToMapUnits(double x, double y)
        {
            return ScreenToMapUnits(x, y, false);
        }

        private PointF ScreenToMapUnits(double x, double y, bool allowOutsideWindow)
        {
            if (!allowOutsideWindow)
            {
                if (x > this.Width - 1) x = this.Width - 1;
                else if (x < 0) x = 0;

                if (y > this.Height - 1) y = this.Height - 1;
                else if (y < 0) y = 0;
            }

            x = _extX1 + (_extX2 - _extX1) * (x / this.Width);
            y = _extY1 - (_extY1 - _extY2) * (y / this.Height);
            return new PointF((float)x, (float)y);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string name)
        {
            var handler = this.PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(name));
        }
    }
}
